Project Name: Dino Runner Game
Category: Gaming, Hardware Control, Animation
Difficulty: Intermediate
This project recreates the famous “No Internet” Chrome Dinosaur game on your ESP32! Using a 16×2 LCD, you will animate a running dinosaur that must jump over cactus and dodge birds. The game features sound effects, a high-score system, and even an “Autoplay” indicator that tells you exactly when to jump.
Learning Objectives (What You Will Learn)
By building this project, you will learn to:
- Create Custom Graphics: Design your own characters (Dino, Cactus, Birds) using bitmasks for the LCD.
- Manage Game Physics: Understand how to “shift” terrain and handle gravity/jumping logic in code.
- Use Interrupts: Use the ESP32’s hardware interrupts to make the jump button feel instant and responsive.
- Generate Sound Effects: Use PWM to play different tones for jumping, scoring, and crashing.
- Build a High Score System: Save and display your best performance during a gaming session.
Circuit Connections

| Components | ESP32 Dev Module | Function |
| LCD I2C SDA | SDA | Sends the game graphics and text to the screen. |
| LCD I2C SCL | SCL | The timing signal for the LCD communication. |
| PUSH BUTTON | IO15 | The “Jump” button. Triggers an interrupt to make the Dino hop. |
| SPEAKER | IO25 | Plays “Beep-boop” sounds for jumps and “Game Over” tunes. |
| RED LED | IO4 | (Optional) Lights up to signal when the Dino should jump. |
Code Lab
Step 1: Install Libraries
Before uploading the code, ensure you have the following library installed in your Arduino IDE Library Manager:
- LiquidCrystal_I2C by Frank de Brabander (for the LCD screen)
- Wire.h (Built-in)

Step 2: Code
Copy and paste this code into your Arduino IDE:
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
// ================= PINS =================
#define PIN_BUTTON 15
#define LED 4
#define PIN_SPEAKER 25
// ================= LCD =================
LiquidCrystal_I2C lcd(0x27, 16, 2);
// ================= SPRITES =================
#define SPRITE_DINO_RUN1 1
#define SPRITE_DINO_RUN2 2
#define SPRITE_DINO_JUMP 3
#define SPRITE_CACTUS_SMALL 4
#define SPRITE_CACTUS_LARGE 5
#define SPRITE_BIRD1 6
#define SPRITE_BIRD2 7
#define SPRITE_GROUND 8
#define DINO_HORIZONTAL_POSITION 1
#define TERRAIN_WIDTH 16
// Obstacle types
#define OBSTACLE_NONE 0
#define OBSTACLE_CACTUS_SMALL 1
#define OBSTACLE_CACTUS_LARGE 2
#define OBSTACLE_BIRD_LOW 3
#define OBSTACLE_BIRD_HIGH 4
// Dino states
#define DINO_RUN1 0
#define DINO_RUN2 1
#define DINO_JUMP_START 2
#define DINO_JUMP_MID 3
#define DINO_JUMP_HIGH 4
#define DINO_JUMP_FALL 5
// Game state
static char terrainUpper[TERRAIN_WIDTH + 1];
static char terrainLower[TERRAIN_WIDTH + 1];
volatile bool buttonPushed = false;
static unsigned int highScore = 0;
#define BASE_SPEED 100
#define MIN_SPEED 50
// ================= MUSIC =================
#define NOTE_C4 262
#define NOTE_D4 294
#define NOTE_E4 330
#define NOTE_G4 392
#define NOTE_C5 523
#define NOTE_E5 659
void playJumpSound() {
tone(PIN_SPEAKER, NOTE_C5, 50);
delay(50);
tone(PIN_SPEAKER, NOTE_E5, 50);
}
void playCollisionSound() {
tone(PIN_SPEAKER, NOTE_G4, 100);
delay(100);
tone(PIN_SPEAKER, NOTE_D4, 100);
delay(100);
tone(PIN_SPEAKER, NOTE_C4, 200);
}
// ================= GRAPHICS =================
void initializeGraphics() {
static byte graphics[] = {
B00000,B00111,B00111,B00111,B01111,B11111,B01101,B01100, // Run 1
B00000,B00111,B00111,B00111,B01111,B11111,B01110,B00110, // Run 2
B00000,B00111,B00111,B00111,B01111,B11111,B01100,B01000, // Jump
B00100,B00101,B10101,B10101,B11111,B01110,B00100,B00100, // Cactus S
B00100,B10101,B10101,B10101,B11111,B01110,B00100,B00100, // Cactus L
B00001,B00001,B00001,B00011,B11111,B00000,B00000,B00000, // Bird 1
B00000,B00001,B00011,B01111,B11111,B00000,B00000,B00000, // Bird 2
B11111,B00000,B00000,B00000,B00000,B00000,B00000,B00000, // Ground
};
for (int i = 0; i < 8; i++) {
lcd.createChar(i + 1, &graphics[i * 8]);
}
for (int i = 0; i < TERRAIN_WIDTH; i++) {
terrainUpper[i] = ' ';
terrainLower[i] = (i == 0) ? ' ' : SPRITE_GROUND;
}
}
void shiftTerrain() {
for (int i = 0; i < TERRAIN_WIDTH - 1; i++) {
terrainUpper[i] = terrainUpper[i + 1];
terrainLower[i] = terrainLower[i + 1];
}
terrainUpper[TERRAIN_WIDTH - 1] = ' ';
terrainLower[TERRAIN_WIDTH - 1] = SPRITE_GROUND;
}
void addObstacle(byte obstacleType) {
switch (obstacleType) {
case OBSTACLE_CACTUS_SMALL: terrainLower[TERRAIN_WIDTH - 1] = SPRITE_CACTUS_SMALL; break;
case OBSTACLE_CACTUS_LARGE: terrainLower[TERRAIN_WIDTH - 1] = SPRITE_CACTUS_LARGE; break;
case OBSTACLE_BIRD_LOW: terrainLower[TERRAIN_WIDTH - 1] = SPRITE_BIRD1; break;
case OBSTACLE_BIRD_HIGH: terrainUpper[TERRAIN_WIDTH - 1] = SPRITE_BIRD1;
terrainLower[TERRAIN_WIDTH - 1] = ' '; break;
}
}
bool drawDino(byte dinoState, unsigned int score) {
bool collide = false;
char upperSave = terrainUpper[DINO_HORIZONTAL_POSITION];
char lowerSave = terrainLower[DINO_HORIZONTAL_POSITION];
switch (dinoState) {
case DINO_RUN1: terrainLower[DINO_HORIZONTAL_POSITION] = SPRITE_DINO_RUN1;
collide = (lowerSave != ' ' && lowerSave != SPRITE_GROUND); break;
case DINO_RUN2: terrainLower[DINO_HORIZONTAL_POSITION] = SPRITE_DINO_RUN2;
collide = (lowerSave != ' ' && lowerSave != SPRITE_GROUND); break;
case DINO_JUMP_HIGH: terrainUpper[DINO_HORIZONTAL_POSITION] = SPRITE_DINO_JUMP;
terrainLower[DINO_HORIZONTAL_POSITION] = ' ';
collide = (upperSave != ' '); break;
default: terrainLower[DINO_HORIZONTAL_POSITION] = SPRITE_DINO_JUMP;
collide = (lowerSave != ' ' && lowerSave != SPRITE_GROUND); break;
}
lcd.setCursor(0, 0); lcd.print(terrainUpper);
lcd.setCursor(0, 1); lcd.print(terrainLower);
String s = String(score); lcd.setCursor(16-s.length(), 0); lcd.print(s);
terrainUpper[DINO_HORIZONTAL_POSITION] = upperSave;
terrainLower[DINO_HORIZONTAL_POSITION] = lowerSave;
return collide;
}
void IRAM_ATTR buttonPush() { buttonPushed = true; }
void setup() {
pinMode(PIN_BUTTON, INPUT_PULLUP);
pinMode(LED, OUTPUT);
attachInterrupt(digitalPinToInterrupt(PIN_BUTTON), buttonPush, FALLING);
lcd.init(); lcd.backlight();
initializeGraphics();
lcd.print(" LCD DINO RUNNER");
delay(1500);
}
void loop() {
static byte dinoState = DINO_RUN1;
static bool playing = false;
static unsigned int distance = 0;
static int obstacleTimer = 20;
if (!playing) {
lcd.setCursor(0,0); lcd.print(" DINO RUNNER! ");
lcd.setCursor(0,1); lcd.print(" Press to Start ");
if (buttonPushed) {
playing = true; distance = 0; buttonPushed = false; initializeGraphics();
lcd.clear();
}
return;
}
shiftTerrain();
if (--obstacleTimer <= 0) {
addObstacle(random(1, 5));
obstacleTimer = 10 + random(10);
}
if (buttonPushed && dinoState < DINO_JUMP_START) {
dinoState = DINO_JUMP_START; playJumpSound(); buttonPushed = false;
}
if (dinoState == DINO_RUN1) dinoState = DINO_RUN2;
else if (dinoState == DINO_RUN2) dinoState = DINO_RUN1;
else if (dinoState >= DINO_JUMP_START && dinoState < DINO_JUMP_FALL) dinoState++;
else dinoState = DINO_RUN1;
if (drawDino(dinoState, distance / 2)) {
playing = false; playCollisionSound();
lcd.clear(); lcd.print(" GAME OVER!"); delay(2000);
} else {
distance++;
delay(max(50, 100 - (int)(distance/50)));
}
}
Step 3: Upload and Run
- Select ESP32 Dev Module in your Arduino IDE.
- Connect your ESP32 to your computer via USB.
- Click Upload.
How to Play Dino Runner
- Step 1: Power Up The screen will flash “LCD DINO RUNNER”. After a moment, it will prompt you to “Press to Start”.
- Step 2: The Run The Dino will begin running on the bottom row. Cactus will appear on the bottom, and birds will appear on either the bottom or top row.
- Step 3: Jump! * Small/Large Cactus: Press the button to jump over them.
- Low Birds: You must jump over these.
- High Birds: Do nothing! You can run safely underneath them.
- Step 4: Speed and Score The game gets faster the longer you survive! Your score is displayed in the top-right corner. If you hit an obstacle, the game ends, and your High Score will be recorded.
Troubleshooting Guide
| Problem | Simple |
| Screen is blank or too dark. | Turn the blue potentiometer on the back of the LCD. |
| Dino doesn’t jump. | Ensure the button is connected to IO15 and the other side to GND. |
| No sound effects. | Check if the speaker is on IO25. Ensure it is a Piezo speaker. |
| Game is way too fast. | Check the delay calculation at the bottom of the loop. |
Tip: If you are finding the game too hard, look for the AUTOPLAY LED (IO4). It flashes briefly right before an obstacle hits the Dino’s position, acting as a “reflex trainer”!
Buy from:
Myduino AIoT Education Kit from myduino.com






